React Suspense and Concurrent Rendering Concepts
React Suspense and Concurrent Rendering are important concepts in modern React development. Suspense helps display fallback UI while supported content is loading, while Concurrent Rendering allows React to prepare UI updates in an interruptible way so the application can remain responsive. Together with features such as lazy(), startTransition(), and useTransition(), they help developers build smoother and more responsive applications.
React JS Training: JustAcademy React JS Training Course
Course Demo: Register for React JS Course Demo
1. Learning Objectives
After completing this topic, you will be able to:
- Understand React Suspense and Concurrent Rendering.
- Display fallback UI while supported content is loading.
- Lazy-load React components to reduce initial JavaScript loading.
- Understand how concurrent rendering improves UI responsiveness.
- Use
startTransition() and useTransition() to manage non-urgent updates.
- Understand how Suspense boundaries work.
- Build loading states using nested Suspense boundaries.
- Recognize common mistakes and apply React performance best practices.
2. What Is React Suspense?
Suspense is a built-in React component that displays fallback content when its children suspend while waiting for supported resources or data. When the required content becomes ready, React replaces the fallback with the actual UI.
Suspense is commonly used for lazy-loaded components and for data loading through Suspense-enabled frameworks or supported Promise-reading APIs.
Basic Syntax
import { Suspense } from "react";
<Suspense fallback={<p>Loading...</p>}>
<MyComponent />
</Suspense>
Important Terms
| Term |
Description |
| Suspense |
A React component that coordinates loading states for supported suspended content. |
| Fallback |
The temporary UI shown while the content is not ready. |
| Children |
The actual components or content that React attempts to render. |
| Suspense boundary |
A particular Suspense component that controls the fallback for its descendant content. |
3. Why Is Suspense Important?
Modern applications often load different parts of the UI at different times. For example, a dashboard may display a navigation bar immediately while loading a chart, profile information, or activity feed.
Without carefully designed loading states, users may see blank areas or confusing page transitions. Suspense allows developers to define which part of the interface should display a fallback until its content is ready.
Benefits of Suspense
- Better user experience: Displays meaningful loading UI instead of an empty region.
- Code splitting: Works with
lazy() to load components when needed.
- Organized loading states: Lets developers coordinate the reveal of related components.
- Nested loading boundaries: Allows independent parts of the UI to load separately.
- Server rendering support: Integrates with React's streaming server-rendering capabilities.
- Transition support: Can help avoid replacing already visible content with a fallback during non-urgent updates.
4. Example: Suspense with a Lazy-Loaded Component
React's lazy() function loads a component's code when React first attempts to render it. The lazy-loaded component should be wrapped in a Suspense boundary so React has a fallback to display while the component code is loading.
Step 1: Create Dashboard.jsx
export default function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
<p>Welcome to your dashboard.</p>
</div>
);
}
Step 2: Use lazy() and Suspense in App.jsx
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./Dashboard"));
export default function App() {
return (
<div>
<h1>My Application</h1>
<Suspense fallback={<p>Loading dashboard...</p>}>
<Dashboard />
</Suspense>
</div>
);
}
How It Works
- React loads the main application.
- React attempts to render the lazy-loaded Dashboard component.
- If its module is still loading, the Suspense boundary displays the fallback.
- When the module becomes available, React renders the Dashboard component.
Important: A fallback is only displayed when the content actually suspends. A very fast load may make the fallback difficult to notice.
5. Understanding Suspense Fallbacks
The fallback prop accepts any valid React node, such as text, a spinner, a skeleton screen, or a placeholder component.
Example: Custom Loading Component
function Loading() {
return (
<div className="loading-container">
<div className="spinner" aria-hidden="true"></div>
<p role="status">Loading content, please wait...</p>
</div>
);
}
export default Loading;
Use the component as the fallback:
import { Suspense, lazy } from "react";
import Loading from "./Loading";
const Reports = lazy(() => import("./Reports"));
export default function App() {
return (
<Suspense fallback={<Loading />}>
<Reports />
</Suspense>
);
}
Common Fallback Options
| Fallback Type |
Use Case |
| Text message |
Simple applications and small components. |
| Spinner |
Short loading periods. |
| Skeleton screen |
Dashboards, profile cards, and content lists. |
| Placeholder layout |
Large page sections where the final layout is predictable. |
6. What Is Concurrent Rendering?
Concurrent Rendering is part of React's rendering architecture that allows React to prepare UI updates in an interruptible way. React can start rendering an update, pause it when necessary, continue later, or abandon that render attempt if a newer update makes it unnecessary.
React waits until it is ready to commit a consistent result before applying the completed update to the DOM. This helps React prioritize responsive interactions while preparing more expensive UI changes.
Key Characteristics
- React can prepare UI updates without immediately committing every intermediate result.
- Some rendering work can be interrupted by more urgent work.
- React can prioritize responsive interactions over non-urgent rendering work.
- React can abandon outdated render work and prepare a newer result.
- Users see a consistent committed UI rather than partially applied render attempts.
Important: Concurrent Rendering is not a separate Hook or component that you manually switch on. It is an underlying React capability used by features such as transitions and Suspense.
7. Why Concurrent Rendering Matters
Consider an application with a search box that filters thousands of products. Updating the input value should feel immediate, but rendering a large filtered list may take more time.
Concurrent features let developers mark the expensive update as non-urgent, allowing React to prioritize the user's immediate input while preparing the results.
Benefits
- Responsive interactions: Important user input can remain responsive during expensive UI updates.
- Prioritized rendering: Urgent and non-urgent updates can be treated differently.
- Improved navigation: Transitions can help preserve existing content while new content loads.
- Flexible rendering: React can interrupt or abandon render work that is no longer needed.
- Better loading experiences: Concurrent features work together with Suspense to coordinate when content appears.
8. Concurrent Rendering vs Traditional Synchronous Rendering
| Traditional Synchronous Rendering |
Concurrent Rendering |
| Rendering work proceeds without interruption once that rendering task begins. |
React may pause, resume, or abandon rendering work. |
| Expensive rendering can delay other work on the main thread. |
React can prioritize urgent updates between units of render work. |
| Developers have fewer mechanisms for marking updates as non-urgent. |
Transitions and other concurrent features help express update priority. |
| UI updates still need to produce a consistent committed result. |
React also commits a consistent result rather than exposing partial render work. |
Concurrent rendering does not mean that JavaScript rendering automatically runs on another thread. React still performs rendering work on the JavaScript thread, but it can schedule and interrupt that work to improve responsiveness.
9. What Is startTransition()?
startTransition() is a React API that marks state updates inside its callback as non-urgent transitions. It is useful when a UI update may require expensive rendering, such as changing a selected tab, navigating between views, or filtering a large list.
Syntax
import { startTransition } from "react";
startTransition(() => {
setState(nextState);
});
Example: Switching Tabs
import { startTransition, useState } from "react";
export default function Tabs() {
const [tab, setTab] = useState("home");
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onClick={() => selectTab("home")}>Home</button>
<button onClick={() => selectTab("reports")}>Reports</button>
<button onClick={() => selectTab("settings")}>Settings</button>
<h2>Selected tab: {tab}</h2>
</div>
);
}
This example illustrates the API. The benefit is most noticeable when changing the tab causes substantial rendering work or causes content to suspend.
Important Notes
startTransition() does not delay the callback with a timer.
- It marks synchronous state updates scheduled inside the callback as transition updates.
- It does not automatically make a slow calculation faster.
- State updates after an asynchronous boundary may need to be wrapped in another
startTransition(), depending on where they are scheduled.
- Do not use transition updates for controlled text input state that must update immediately.
10. What Is useTransition()?
useTransition() is a React Hook that lets a component mark updates as transitions and track whether the transition is pending.
Syntax
const [isPending, startTransition] = useTransition();
| Return Value |
Description |
isPending |
A Boolean indicating whether the transition is pending. |
startTransition |
A function used to mark state updates as non-urgent transitions. |
Example: Pending Indicator
import { useState, useTransition } from "react";
export default function TabPanel() {
const [tab, setTab] = useState("home");
const [isPending, startTransition] = useTransition();
function changeTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onClick={() => changeTab("home")}>Home</button>
<button onClick={() => changeTab("reports")}>Reports</button>
{isPending && <p role="status">Updating tab...</p>}
<h2>Current tab: {tab}</h2>
</div>
);
}
Explanation: The component uses isPending to display a status message while the transition is pending. In this small example, the update may complete too quickly for the message to be noticeable.
11. Suspense and Concurrent Rendering Together
Suspense and Concurrent Rendering solve related but different problems:
- Suspense: Defines what React should display when supported content is not ready.
- Concurrent Rendering: Gives React the ability to schedule and interrupt rendering work.
- Transitions: Mark selected state updates as non-urgent, helping React avoid disruptive loading states in appropriate situations.
Example: Lazy-Loaded Reports
import { lazy, Suspense, useState, useTransition } from "react";
const Reports = lazy(() => import("./Reports"));
const Overview = lazy(() => import("./Overview"));
export default function App() {
const [page, setPage] = useState("overview");
const [isPending, startTransition] = useTransition();
function navigate(nextPage) {
startTransition(() => {
setPage(nextPage);
});
}
return (
<div>
<button onClick={() => navigate("overview")}>Overview</button>
<button onClick={() => navigate("reports")}>Reports</button>
{isPending && <p role="status">Preparing page...</p>}
<Suspense fallback={<p>Loading page...</p>}>
{page === "overview" ? <Overview /> : <Reports />}
</Suspense>
</div>
);
}
When a user switches pages, React treats the update as a transition. If the new page suspends, React can keep already revealed content visible while preparing the next UI, rather than immediately replacing the existing content with a fallback. Newly mounted Suspense boundaries may still display their own fallback.
12. Nested Suspense Boundaries
Nested Suspense boundaries allow different parts of a page to reveal independently. A parent boundary can control the main page content, while a child boundary controls a specific section such as a chart or activity feed.
Example
import { Suspense, lazy } from "react";
const Profile = lazy(() => import("./Profile"));
const ActivityFeed = lazy(() => import("./ActivityFeed"));
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading dashboard...</p>}>
<Profile />
<Suspense fallback={<p>Loading activity...</p>}>
<ActivityFeed />
</Suspense>
</Suspense>
</div>
);
}
How Nested Boundaries Work
- The outer Suspense boundary controls the main section.
- The inner boundary controls the activity feed.
- The activity feed can display its own fallback independently when the outer content is ready.
- The final layout appears progressively as the relevant content becomes available.
Best practice: Place boundaries according to the intended user experience rather than wrapping every individual component in Suspense.
13. Suspense and Data Fetching
Suspense supports certain data-loading patterns, but it does not automatically detect every asynchronous operation in a component.
Supported Suspense Scenarios
- Lazy-loaded components using
lazy().
- Reading supported Promises using React's
use() API.
- Data loading integrated with a Suspense-enabled framework or library.
- Supported streaming server-rendering scenarios.
What Does Not Automatically Trigger Suspense?
Fetching data inside a regular useEffect() does not automatically activate a Suspense boundary.
useEffect(() => {
fetch("/api/products")
.then(response => response.json())
.then(data => setProducts(data));
}, []);
This is a normal Effect-based fetching pattern. You must manage its loading and error states yourself unless you use a data-loading approach that integrates with Suspense.
Example: Reading a Promise with use()
React's use() API can read a Promise in a supported Suspense-enabled setup. The Promise should be created and cached appropriately rather than recreated on every render.
import { Suspense, use } from "react";
function ProductDetails({ productPromise }) {
const product = use(productPromise);
return (
<div>
<h2>{product.name}</h2>
<p>Price: {product.price}</p>
</div>
);
}
export default function App({ productPromise }) {
return (
<Suspense fallback={<p>Loading product...</p>}>
<ProductDetails productPromise={productPromise} />
</Suspense>
);
}
This example assumes that productPromise is supplied by a supported data-loading layer or another appropriate Promise source. Suspense does not itself create the Promise, fetch the data, or provide caching.
14. Suspense vs useEffect Loading
| Feature |
Suspense-Based Loading |
Effect-Based Loading |
| Loading UI |
Defined through a Suspense fallback. |
Usually managed with local state. |
| Data integration |
Requires a Suspense-enabled source or supported API. |
Can use ordinary asynchronous requests inside Effects. |
| Code splitting |
Works directly with lazy(). |
Does not handle lazy component loading by itself. |
| Error handling |
Use an Error Boundary or framework-specific handling where appropriate. |
Handle request errors and store them in state. |
| Setup complexity |
Depends on the framework or data source. |
Can be straightforward for simple requests but requires careful cleanup and race handling. |
15. What Is useDeferredValue()?
useDeferredValue() is a React Hook that allows a part of the UI to use a deferred version of a value. It is useful when updating one part of the UI immediately while allowing an expensive section to catch up later.
Syntax
const deferredValue = useDeferredValue(value);
Example: Search Filtering
import { useDeferredValue, useMemo, useState } from "react";
const products = [
"Laptop",
"Keyboard",
"Mouse",
"Monitor",
"Headphones",
"Webcam"
];
export default function ProductSearch() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [deferredQuery]);
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="Search products"
/>
<ul style={{ opacity: isStale ? 0.6 : 1 }}>
{filteredProducts.map(product => (
<li key={product}>{product}</li>
))}
</ul>
</div>
);
}
The input state updates immediately, while the expensive filtered section can use the deferred query. In this small example, the difference may not be noticeable because the product list is short.
16. startTransition() vs useTransition() vs useDeferredValue()
| API |
Main Purpose |
Typical Use |
startTransition() |
Marks state updates as non-urgent. |
Navigation or tab changes. |
useTransition() |
Marks updates as transitions and exposes pending status. |
Showing a pending indicator during an update. |
useDeferredValue() |
Provides a deferred version of a value. |
Keeping a search input responsive while results update. |
Suspense |
Defines fallback UI for supported suspended content. |
Lazy-loaded components or Suspense-enabled data. |
17. Common Mistakes and Solutions
| Mistake |
Why It Is a Problem |
Solution |
| Assuming Suspense handles every fetch request |
Ordinary Effect-based fetching does not automatically suspend. |
Use a supported Suspense data source or manage loading state explicitly. |
| Forgetting a Suspense boundary around a lazy component |
The component has no appropriate local fallback boundary. |
Wrap the component tree with Suspense. |
| Using an overly large boundary |
A small suspended section may replace too much visible content with a fallback. |
Use boundaries that match meaningful loading sections. |
| Wrapping controlled input state in a transition |
Typing may become delayed or behave incorrectly. |
Update input state immediately and defer expensive results instead. |
| Expecting transitions to make calculations faster |
Transitions affect scheduling priority, not the speed of a calculation itself. |
Optimize expensive calculations and profile performance. |
| Recreating Promise objects during every render |
Repeated suspension or inconsistent loading behavior may occur. |
Use a stable Promise source or a suitable data-fetching framework. |
| Not handling errors |
Suspense fallbacks are not a complete error-handling strategy. |
Use Error Boundaries and appropriate request error handling. |
18. Performance Best Practices
- Use
lazy() for components that do not need to be loaded immediately.
- Choose Suspense boundaries based on meaningful user-facing loading sections.
- Use transitions for non-urgent updates that involve expensive rendering or suspension.
- Keep controlled text input updates urgent.
- Use
useDeferredValue() when a part of the UI can safely lag behind an immediate input.
- Profile before adding memoization or complex optimization logic.
- Use a Suspense-enabled framework or data library for supported data-loading patterns.
- Handle request errors, cancellation, and stale responses correctly.
- Provide accessible loading indicators, such as elements with
role="status".
- Avoid placing Suspense boundaries around every component without a clear loading strategy.
19. Practical Project: Product Dashboard with Suspense
Project Goal: Build a React dashboard that loads separate page sections on demand and keeps interactions responsive during updates.
Project Features
- Lazy-load the Dashboard, Reports, and Settings components.
- Display fallback messages while supported content is loading.
- Use nested Suspense boundaries for independent page sections.
- Use
useTransition() to manage navigation updates.
- Show a pending indicator when a transition is in progress.
- Use
useDeferredValue() for an expensive search or filtering interface.
- Handle loading errors using an Error Boundary and appropriate error UI.
Suggested Folder Structure
src/
main.jsx
App.jsx
Dashboard.jsx
Reports.jsx
Settings.jsx
Loading.jsx
ErrorBoundary.jsx
ProductSearch.jsx
Project Workflow
- Create the React application and its page components.
- Use
lazy() to load page components dynamically.
- Wrap the lazy components in Suspense boundaries.
- Use
useTransition() to mark page navigation as non-urgent.
- Add a pending indicator and appropriate loading placeholders.
- Build a search component using
useDeferredValue().
- Test slow-loading components and error scenarios.
- Use browser developer tools to inspect network requests and performance.
20. Interview Questions and Answers
Q1. What is React Suspense?
Suspense is a React component that displays fallback UI while supported child content is not ready to render.
Q2. What is Concurrent Rendering?
Concurrent Rendering is React's rendering capability that allows certain rendering work to be interrupted, resumed, or abandoned before committing a consistent UI update.
Q3. What is the purpose of the fallback prop?
The fallback prop specifies the temporary UI displayed when the Suspense boundary's children suspend.
Q4. What is React.lazy()?
lazy() allows a component's code to be loaded dynamically when React first attempts to render that component.
Q5. Does Suspense automatically handle every API request?
No. Suspense supports specific loading mechanisms, such as lazy-loaded components and supported Promise-reading APIs. An ordinary fetch inside useEffect() does not automatically trigger Suspense.
Q6. What is startTransition()?
startTransition() marks state updates as non-urgent so React can prioritize more urgent interactions.
Q7. What is useTransition()?
useTransition() is a Hook that provides a transition function and an isPending status.
Q8. What is useDeferredValue()?
useDeferredValue() lets part of the UI use a deferred version of a value so expensive rendering can lag behind more urgent updates.
Q9. What is a Suspense boundary?
A Suspense boundary determines which part of the component tree displays a fallback when supported descendant content suspends.
Q10. Why are nested Suspense boundaries useful?
They let independent sections reveal at different times instead of forcing the entire page to wait for every section.
Q11. Does Concurrent Rendering mean React uses another JavaScript thread?
No. Concurrent Rendering allows React to schedule and interrupt rendering work on the JavaScript thread. It does not automatically move React rendering to a separate thread.
Q12. Should controlled text input updates be placed inside a transition?
No. Controlled text input updates should remain urgent. If rendering the results is expensive, consider using useDeferredValue() for the results instead.
Q13. Can Suspense handle errors?
Suspense is primarily for loading states. Use Error Boundaries or framework-specific error handling to display appropriate UI for rendering errors.
Q14. How does Suspense work with server rendering?
React's streaming server-rendering APIs can use Suspense boundaries to stream the page shell and reveal suspended content as it becomes available.
21. Practice Exercises
- Create a lazy-loaded Profile component and display a fallback while its module loads.
- Build a dashboard with separate Suspense boundaries for the profile and activity feed.
- Create a tab interface that uses
useTransition() to display a pending indicator.
- Build a product search interface using
useDeferredValue().
- Compare an Effect-based fetch with a Suspense-enabled data-loading example.
- Create an Error Boundary for a component that fails to render.
- Experiment with a large list and observe the responsiveness of urgent and non-urgent updates.
- Explain the difference between a Suspense fallback and a transition pending indicator.
22. Quick Revision
Suspense displays fallback UI while supported content suspends.
lazy() supports loading component code on demand.
- A Suspense boundary controls the fallback for its descendant content.
- Concurrent Rendering allows React to interrupt, resume, or abandon rendering work.
startTransition() marks updates as non-urgent.
useTransition() provides a transition function and pending status.
useDeferredValue() allows a part of the UI to use a deferred value.
- Ordinary Effect-based data fetching does not automatically activate Suspense.
- Transitions can help avoid disruptive fallbacks during suitable updates.
- Error Boundaries and request error handling are still important.
- Good Suspense boundaries match meaningful loading sections.
23. Official React Documentation
24. React JS Training Resources
Explore React Hooks, component loading, performance optimization, and modern frontend development through these resources:
Conclusion
React Suspense and Concurrent Rendering help developers create responsive applications with better loading experiences. Suspense coordinates fallback UI for supported content that is not ready, while Concurrent Rendering enables React to schedule and interrupt rendering work. By combining Suspense boundaries with lazy(), startTransition(), useTransition(), and useDeferredValue(), developers can build applications that load progressively and remain responsive during complex updates.